﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using CutAways.Modeling;
using CutAways.Geometry;
using System.Windows.Forms;

namespace CutAways.Illustration
{
	[Serializable]
	class OcclusionGraph
	{
		[Serializable]
		private class Node
		{
			private GeometricObject geometricObject;
			private List<Node> parents;
			private List<Node> successors;
			
			public List<Node> Parents
			{
				get { return parents; }
			}
			public List<Node> Successors
			{
				get { return successors; }
			}
			public GeometricObject GeometricObject
			{
				get { return geometricObject; }
			}
			public Node(GeometricObject geometricObject)
			{
				this.geometricObject = geometricObject;
				parents = new List<Node>();
				successors = new List<Node>();
			}
		}

		#region Private fields

		private readonly Model model;
		private readonly Camera viewpoint;

		private readonly float visibilityThreshold;
		private readonly float occlusionThreshold;

		private Dictionary<GeometricObject, int> pixelCounts;
		private Dictionary<GeometricObject, Node> objectMap;
		private List<List<GeometricObject>> layers;

		private int edgeCount = 0; // ***

		#endregion

		#region Public properties

		public Model Model
		{
			get { return model; }
		}
		public Camera Viewpoint
		{
			get { return viewpoint; }
		}

		public GeometricObject[] Roots
		{
			get { return layers.Count > 0 ? layers[0].ToArray() : null; }
		}

		public List<List<GeometricObject>> Layers
		{
			get { return layers; }
		}

		public int EdgeCount // ***
		{
			get { return edgeCount; }
		}

		#endregion

		#region Constructors

		/// <summary>
		/// Bezparametricky konstruktor
		/// </summary>
		public OcclusionGraph(Model model, Camera viewpoint)
		{
			this.model = model;
			this.viewpoint = viewpoint;
			pixelCounts = new Dictionary<GeometricObject, int>();
			objectMap = new Dictionary<GeometricObject, Node>();
			layers = new List<List<GeometricObject>>();
			
			// nastav parametry
			visibilityThreshold = 0.9f;
			occlusionThreshold = 0.1f; // v tom clanku je zde 0.2f
			
			build(); // vytvor graf
		}

		#endregion

		#region Public methods

		public List<GeometricObject> GetParentsOf(GeometricObject obj)
		{
			List<GeometricObject> parents = new List<GeometricObject>();
			foreach (Node n in objectMap[obj].Parents)
				parents.Add(n.GeometricObject);
			return parents;
		}

		public List<GeometricObject> GetSuccessorsOf(GeometricObject obj)
		{
			List<GeometricObject> successors = new List<GeometricObject>();
			foreach (Node n in objectMap[obj].Successors)
				successors.Add(n.GeometricObject);
			return successors;
		}

		public int? GetDistanceBetween(GeometricObject occludee, GeometricObject occluder)
		{
			if (occludee == occluder)
				return 0;
			List<GeometricObject> parents = GetParentsOf(occludee);
			if (parents.Count == 0)
				return null;
			List<int> results = new List<int>();
			foreach (GeometricObject p in parents)
			{
				int? x = GetDistanceBetween(p, occluder);
				if (x.HasValue)
					results.Add(x.Value + 1);
			}
			if (results.Count == 0)
				return null;
			return results.Min(); // or Max ?? /**/
		}

		public int? GetDepthOf(GeometricObject o)
		{
			for (int i = 0; i < layers.Count; i++)
				for (int j = 0; j < layers[i].Count; j++)
					if (o == layers[i][j])
						return i;
			return null;
		}

		#endregion

		#region Private methods

		/// <summary>
		/// vytvori graf zakrytu objektu v modelu(model) z urciteho pohledu(viewpoint)
		/// </summary>
		private void build()
		{
			// vytvor uzly
			foreach (GeometricObject o in model.Objects)
				objectMap[o] = new Node(o);

			// spocti plochy objektu
			computePixelCounts();

			// vytvor vrstvy viditelnosti
			buildVisibilityLayers();
			
			// vytvor graf
			for (int i = layers.Count - 1; i >= 1; i--)
			{
				foreach (GeometricObject occludee in layers[i])
				{
					float occlusionCounter = 0f;
					List<GeometricObject> occluders = new List<GeometricObject>();
					// pro kazdy objekt projdi vsemi vrstvami, zacni vrstvou, kde se tento objekt nachazi a pokracuj smerem k pozorovateli
					// pro kazde dva objekty vypocti procento prekryti
					bool breakIteration = false;
					for (int j = i - 1; j >= 0; j--)
					{
						foreach (GeometricObject newOccluder in layers[j])
						{
							occluders.Add(newOccluder);
							float ratio = computeOcclusion(occludee, occluders);
							if (ratio > occlusionCounter)
							{
								float increment = ratio - occlusionCounter;
								if (increment >= occlusionThreshold) // je v zakrytu, tak pridam hranu do grafu
								{
									//if (!objectMap[occludee].Parrents.Contains(objectMap[occluder]))
									objectMap[occludee].Parents.Add(objectMap[newOccluder]);
									//if (!objectMap[occluder].Successors.Contains(objectMap[occludee]))
									objectMap[newOccluder].Successors.Add(objectMap[occludee]);
									edgeCount++; // ***
									occlusionCounter = ratio;
									if (1f - occlusionCounter < occlusionThreshold) // uz to nic dalsiho nemuze zakryt
									{
										breakIteration = true;
										break;
									}
								}
							}
						}
						if (breakIteration)
							break;
					}
				

				}
			}
		}

		private void computePixelCounts()
		{
			foreach(GeometricObject o in model.Objects)
			{
				if (!o.Visible) // pokud se nema vykreslovat, tak s nim nepocitat, je z nej videt 0 pixelu...
					pixelCounts[o] = 0;
				else
					pixelCounts[o] = OcclusionQuery.GetPixelCount(model, o, viewpoint);
			}
		}

		private float computeOcclusion(GeometricObject occludee, List<GeometricObject> occluders)
		{
			int[] samples = OcclusionQuery.GetPixelCounts(model, new GeometricObject[] { occludee }, occluders.ToArray(), viewpoint);
			return 1.0f - ((float)samples[0] / (float)pixelCounts[occludee]);
		}

		/// <summary>
		/// opakuje tyto kroky, dokud nejsou vsechny objekty umisteny do prislusne vrstvy
		/// 1. Render all parts in S
		/// 2. Create new layer containing visible parts
		/// 3. Remove visible parts from S
		/// </summary>
		private void buildVisibilityLayers()
		{
			List<GeometricObject> remainingObjects = new List<GeometricObject>();
			foreach (GeometricObject o in pixelCounts.Keys) // pridej objekty, ktere nejsou uplne mimo
				if (pixelCounts[o] > 0)
					remainingObjects.Add(o);
			while (remainingObjects.Count > 0)
			{
				List<GeometricObject> currentLayer = getVisibleObjects(remainingObjects.ToArray());
				layers.Add(currentLayer); // pridej vrstvu
				foreach (GeometricObject o in currentLayer) // odeber je a se zbytkem pokracuj
					remainingObjects.Remove(o);
			}
		}

		private List<GeometricObject> getVisibleObjects(GeometricObject[] objects)
		{
			List<GeometricObject> visibleObjects = new List<GeometricObject>();
			int[] samples = OcclusionQuery.GetPixelCounts(model, objects, null, viewpoint);
			int objectWithMaxRatio = 0;
			float maxRatio = 0f;
			for (int i = 0; i < objects.Length; i++)
			{
				if (samples[i] == 0)
					continue;
				float ratio = (float)samples[i] / (float)pixelCounts[objects[i]];
				if (ratio >= visibilityThreshold)
					visibleObjects.Add(objects[i]);
				if (ratio > maxRatio)
				{
					maxRatio = ratio;
					objectWithMaxRatio = i;
				}
			}
			if (visibleObjects.Count == 0 && maxRatio > 0f)
				visibleObjects.Add(objects[objectWithMaxRatio]);
			return visibleObjects;
		}

		#endregion

	}
}
